/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.database.engine;

import cn.webwheel.database.IException;
import cn.webwheel.database.annotations.*;
import cn.webwheel.el.GenericClass;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class ResultDesc {

    Map<Method, ResultDesc> subResults;
    ResultDesc listResultDesc;

    String tableName;
    String columnName;
    boolean outerJoin;

    UserTypeInfo<?,?> userTypeInfo;

    static class SResult {
        boolean outerJoin;
        String columeName;
        String userType;
    }

    static Pattern selectPat = Pattern.compile("^\\s*select\\s+(.+?)\\s+from", Pattern.CASE_INSENSITIVE);

    @SuppressWarnings("unchecked")
    ResultDesc(Class<?> ctx, Method method, Map<String, UserTypeInfo> userTypeInfoMap) {
        Class finalClass;
        if (method.getReturnType() == List.class) {
            Type type = method.getGenericReturnType();
            if (!(type instanceof ParameterizedType)) throw new IException("list result must have generic parameter");
            finalClass = new GenericClass(((ParameterizedType) type).getActualTypeArguments()[0]).toClass();
        } else if (method.getReturnType() == Void.TYPE) {
            finalClass = ctx;
        } else {
            finalClass = method.getReturnType();
        }

        UserType userType = method.getAnnotation(UserType.class);
        if (userType != null) {
            userTypeInfo = userTypeInfoMap.get(userType.value());
            if (userTypeInfo == null) {
                throw new IException("can not find user type for \"" + userType.value() + "\"");
            }
            if (method.getReturnType() == Void.TYPE) {
                throw new IException("void method can not have user type");
            }
            if (!finalClass.isAssignableFrom(userTypeInfo.bCls)) {
                throw new IException("user type is not compatible with " + finalClass);
            }
            return;
        } else {
            userTypeInfo = new UserTypeInfo();
            userTypeInfo.bCls = finalClass;
            userTypeInfo.dCls = finalClass;
            if (Utils.isBasicType(finalClass)) {
                return;
            }
        }

        Table table = method.getAnnotation(Table.class);
        if (table != null) tableName = table.value();
        else {
            table = userTypeInfo.bCls.getAnnotation(Table.class);
            if (table != null) {
                tableName = table.value();
            } else {
                tableName = finalClass.getSimpleName();
            }
        }

        List<String> list = new ArrayList<String>();
        Select select = method.getAnnotation(Select.class);
        String result = select.result().trim();
        if (result.isEmpty()) {
            Matcher matcher = selectPat.matcher(select.value());
            if (matcher.find()) {
                String cs = matcher.group(1);
                if (!cs.equals("*")) {
                    String[] ca = cs.split("\\s*,\\s*");
                    StringBuilder sb = new StringBuilder("-* ");
                    for (String c : ca) {
                        sb.append("+").append(c.substring(c.lastIndexOf('.') + 1)).append(' ');
                    }
                    result = sb.toString();
                }
            }
        }
        if (!result.isEmpty()) {
            list.addAll(Arrays.asList(result.split("\\s+")));
            for (String sr : list) {
                if (sr.length() < 2 || (sr.charAt(0) != '+' && sr.charAt(0) != '-' && sr.charAt(0) != '*')) {
                    throw new IException("invalid result description: " + sr + " in " + method);
                }
            }
        }
        init(list, userTypeInfoMap);
    }

    private ResultDesc() {}

    private List<Method> setterMethods() {
        List<Method> methods = new ArrayList<Method>();
        for (Method method : userTypeInfo.bCls.getMethods()) {
            String mn = method.getName();
            if(!mn.startsWith("set")) continue;
            if(mn.length()<4) continue;
            if(method.getParameterTypes().length!=1) continue;
            methods.add(method);
        }
        return methods;
    }

    @SuppressWarnings("unchecked")
    private void init(List<String> srs, Map<String, UserTypeInfo> userTypeInfoMap) {

        Map<String, SResult> srMap = new HashMap<String, SResult>();

        for (String sr : srs) {
            String[] ts = sr.split(":", -1);
            if(ts[0].charAt(0)=='-') {
                String prop = ts[0].substring(1);
                if (prop.equals("*")) {
                    for (Method method : setterMethods()) {
                        String mn = method.getName();
                        String pn = mn.substring(3, 4).toLowerCase() + mn.substring(4);
                        srMap.put(pn, null);
                    }
                } else {
                    srMap.put(prop, null);
                }
                continue;
            }
            if (ts[0].substring(1).equals("*")) {
                for (Method method : setterMethods()) {
                    String mn = method.getName();
                    String pn = mn.substring(3, 4).toLowerCase() + mn.substring(4);
                    SResult srt = new SResult();
                    srt.outerJoin = ts[0].charAt(0)=='*';
                    srMap.put(pn, srt);
                }
                continue;
            }
            SResult srt = new SResult();
            srt.outerJoin = ts[0].charAt(0)=='*';
            String pn = ts[0].substring(1);
            if(ts.length>1) srt.columeName = ts[1];
            if(ts.length>2) srt.userType = ts[2];
            srMap.put(pn, srt);
        }

        subResults = new HashMap<Method, ResultDesc>();

        Set<String> setterNames = new HashSet<String>();

        for (Method method : setterMethods()) {
            String mn = method.getName();
            String pn = mn.substring(3, 4).toLowerCase() + mn.substring(4);
            SResult ad = srMap.get(pn);

            if(srMap.containsKey(pn) && ad==null) continue;
            Include inc = method.getAnnotation(Include.class);
            if(ad==null && inc!=null && !inc.value()) continue;

            if(setterNames.contains(method.getName())) {
                throw new IException("more the one setters named " + method.getName() + " in " + userTypeInfo.bCls);
            }
            setterNames.add(method.getName());

            ResultDesc subRd = new ResultDesc();

            //finalClass
            Class finalClass;
            Class<?> pt = method.getParameterTypes()[0];
            if (pt == List.class) {
                if(ad==null && inc==null) continue;
                if (listResultDesc != null) {
                    throw new IException("multiple parallel list properties is not supported");
                }
                listResultDesc = subRd;
                Type type = method.getGenericParameterTypes()[0];
                if (!(type instanceof ParameterizedType)) {
                    throw new IException("list setter must have generic parameter");
                }
                finalClass = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
            } else {
                finalClass = pt;
            }
            {
                String userType = null;
                UserType ut = method.getAnnotation(UserType.class);
                if (ut != null) userType = ut.value();
                if (ad != null && ad.userType != null) userType = ad.userType;
                if (userType != null) {
                    subRd.userTypeInfo = userTypeInfoMap.get(userType);
                    if (subRd.userTypeInfo == null) {
                        throw new IException("can not find user type for \"" + userType + "\"");
                    }
                    if (!finalClass.isAssignableFrom(subRd.userTypeInfo.bCls)) {
                        throw new IException("user type " + subRd.userTypeInfo.userType.getClass() + " is not compatible with " + finalClass);
                    }
                } else {
                    subRd.userTypeInfo = new UserTypeInfo();
                    subRd.userTypeInfo.bCls = finalClass;
                    subRd.userTypeInfo.dCls = finalClass;
                }
            }

            if(ad==null && inc==null && Utils.isIgnoreDef(subRd.userTypeInfo.dCls)) continue;

            subResults.put(method, subRd);

            if(Utils.isBasicType(subRd.userTypeInfo.dCls)) {
                //columName
                if(ad!=null && ad.columeName!=null) subRd.columnName = ad.columeName;
                else {
                    Column column = method.getAnnotation(Column.class);
                    if(column!=null) subRd.columnName = column.value();
                    else subRd.columnName = pn;
                }
                if(subRd.columnName.startsWith(".")) subRd.columnName = subRd.columnName.substring(1);
                else if(subRd.columnName.indexOf('.')==-1 && tableName.length()>0) subRd.columnName = tableName + "." + subRd.columnName;
                continue;
            } else {
                if(ad!=null && ad.columeName!=null) subRd.tableName = ad.columeName;
                else {
                    Table table = subRd.userTypeInfo.bCls.getAnnotation(Table.class);
                    if (table != null) {
                        subRd.tableName = table.value();
                    } else {
                        subRd.tableName = subRd.userTypeInfo.bCls.getSimpleName();
                    }
                }
            }

            //outerJoin
            if(ad!=null && ad.outerJoin) subRd.outerJoin = true;
            else {
                OuterJoin oj = method.getAnnotation(OuterJoin.class);
                if(oj!=null) subRd.outerJoin = true;
            }

            List<String> nsrs = new ArrayList<String>(srs.size());
            String s = pn + ".";
            for (String sr : srs) {
                if (sr.substring(1).startsWith(s)) {
                    nsrs.add(sr.charAt(0) + sr.substring(1+s.length()));
                }
            }

            subRd.init(nsrs, userTypeInfoMap);
            if (listResultDesc != null && listResultDesc != subRd && subRd.listResultDesc != null) {
                throw new IException("multiple parallel list properties is not supported");
            }
            if(listResultDesc==null) listResultDesc = subRd.listResultDesc;
        }
    }

    boolean isFlat() {
        return listResultDesc==null;
    }

    public UserTypeInfo<?, ?> getUserTypeInfo() {
        return userTypeInfo;
    }

    public boolean isOuterJoin() {
        return outerJoin;
    }

    public String getColumnName() {
        return columnName;
    }

    public Map<Method, ResultDesc> getSubResults() {
        return subResults;
    }

    public boolean hasSubResults() {
        return subResults!=null;
    }

    public ResultDesc getListResultDesc() {
        return listResultDesc;
    }

//    private String toString(int deep) {
//        StringBuilder sb = new StringBuilder();
//        for(int i=0; i<deep; i++) sb.append("  ");
//        sb.append("ResultDesc{" +
//                "tableName='" + tableName + '\'' +
//                ", columnName='" + columnName + '\'' +
//                ", outerJoin=" + outerJoin +
//                ", finalClass=" + userTypeInfo. +
//                '}');
//        if (subResults != null) {
//            for (Map.Entry<Method, ResultDesc> entry : subResults.entrySet()) {
//                sb.append("\n");
//                sb.append(entry.getValue().toString(deep+1));
//            }
//        }
//        return sb.toString();
//    }

//    public String toString() {
//        return toString(0);
//    }
}
